package com.xyzboom.compiler.compiler.lr

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.xyzboom.compiler.NoArg
import com.xyzboom.compiler.compiler.grammar.*
import com.xyzboom.compiler.compiler.lexer.Token
import com.xyzboom.compiler.excepitons.compiler.grammar.UnexpectedNextTokenException
import com.xyzboom.compiler.fa.DFA
import com.xyzboom.compiler.fa.DFANode
import com.xyzboom.compiler.fa.EdgeType.*
import com.xyzboom.compiler.fa.FAEdge
import mu.KotlinLogging


private val logger = KotlinLogging.logger {}

/**
 * Predict table
 *
 * @property productions
 * @property dfa
 * @constructor Create empty Predict table
 */
@NoArg
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
open class PredictTable(
    val productions: ArrayList<Production>,
    val dfa: DFA<Int, ISymbol>,
) {
    @get:JsonIgnore
    val unexpectedNextTokenGenerator:
                (DFANode<Int, ISymbol>, Token?) -> UnexpectedNextTokenException
        get() =
            { stateNow, token ->
                UnexpectedNextTokenException(token, stateNow.next.filter {
                    when (it.key.type) {
                        NORMAL, DOT_ANY, REVERSE, GOTO -> false
                        MOVE_IN, REDUCE -> true
                    }
                }.map { it.key.value })
            }

    /**
     * On reduce
     * callback function when reduce
     * @param reduceSymbols symbols reduced
     * @param nodeAfter astNode after reduce
     * @param reduceIndex index of reduce production
     * @return astNode to add into symbol stack
     */
    open fun onReduce(
        reduceSymbols: ArrayList<ISymbol>, nodeAfter: ASTNode, reduceIndex: Int,
    ): ASTNode {
        return nodeAfter
    }

    /**
     * Generator astNode
     *
     * @param tokens input tokens
     * @param skipTokens ignore these tokens when parse
     * @return root astNode of ast
     */
    fun generatorAST(tokens: List<Token?>, skipTokens: Set<String>): ASTNode {
        val stateStack = ArrayDeque<DFANode<Int, ISymbol>>()
        val symbolStack = ArrayDeque<ISymbol>()
        val astStack = ArrayDeque<ASTNode>()
        stateStack.addLast(dfa.nodeIn)
        var indexNow = 0
        while (true) {
            val stateNow = stateStack.last()
            val tokenNow: Token?
            val terminatorNow = if (indexNow < tokens.size) {
                tokenNow = tokens[indexNow]
                if (tokenNow == null) {
                    indexNow++
                    continue
                }
                Terminator(tokenNow.name)
            } else {
                tokenNow = null
                Terminator.EOF
            }
            if (skipTokens.contains(terminatorNow.tokenName)) {
                indexNow++
                continue
            }
            if (stateNow.next.any {
                    val edge = it.key
                    edge.value == terminatorNow
                }
            ) {
                val moveInState = stateNow.next[FAEdge(terminatorNow, MOVE_IN)]
                if (moveInState != null) {
                    symbolStack.addLast(terminatorNow)
                    astStack.addLast(ASTNode(tokenNow?.name.toString(), tokenNow))
                    stateStack.addLast(moveInState)
                    logger.trace { "meet ${terminatorNow}, move in ${moveInState.value}" }
                } else {
                    val reduceState = stateNow.next[FAEdge(terminatorNow, REDUCE)]
                    if (reduceState == null) {
                        logger.trace { "error in reduce" }
                        throw unexpectedNextTokenGenerator(stateNow, tokenNow)
                    }
                    if (reduceState.value == -1) {
                        logger.trace { "accept" }
                        break
                    }
                    val reduceIndex = reduceState.value
                    val reduceProd = productions[reduceIndex]
                    val astChildren = ArrayDeque<ASTNode>()
                    val reduceSymbols = ArrayList<ISymbol>()
                    reduceSymbols.add(NonTerminator(reduceProd.left.name))
                    for (i in 0 until reduceProd.right.size) {
                        reduceSymbols.add(symbolStack.removeLast())
                        astChildren.addFirst(astStack.removeLast())
                        stateStack.removeLast()
                    }
                    symbolStack.addLast(reduceProd.left)
                    val newASTNode = ASTNode(reduceProd.left.name, null, ArrayList(astChildren))
                    astStack.addLast(onReduce(reduceSymbols, newASTNode, reduceIndex))
                    logger.trace {
                        "meet ${terminatorNow}, reduce, now state stack is ${stateStack.joinToString(", ")}" +
                                "\n\tsymbolStack is ${symbolStack.joinToString(", ")}"
                    }
                    val gotoState = stateStack.last().next[FAEdge(reduceProd.left, GOTO)]
                    if (gotoState == null) {
                        logger.trace { "error in goto" }
                        throw unexpectedNextTokenGenerator(stateNow, tokenNow)
                    }
                    stateStack.addLast(gotoState)
                    logger.trace { "meet ${reduceProd.left}, goto ${gotoState.value}" }
                    continue
                }
            } else {
                throw unexpectedNextTokenGenerator(stateNow, tokenNow)
            }
            indexNow++
        }
        return astStack.last()
    }
}